/*
  S.M.A.C.K - An operating system kernel
  Copyright (C) 2010,2011 Mattias Holm and Kristian Rietveld
  For licensing and a full list of authors of the kernel, see the files
  COPYING and AUTHORS.
*/

#include <vfs/fat16.h>
#include <vfs/vfs.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <stdbool.h>
#include <panic.h>
#include <vm.h>

#define MAX_FS 8

/* FIXME: This code is still assuming to run on little endian.  Intel
 * is little endian, as well is the ARM architecture we are using.
 * When this is going to be ported to a big endian architecture, we
 * need fixes of course.
 */

typedef struct
{
  dev_t *dev;
  dev_minor_t minor;

  fat16_super_t super;

  uint16_t cluster_size;

  off_t fat_table_offset;
  off_t root_offset;
  off_t data_start_offset;

  uint16_t *fat_table;
} fat16_info_t;

static fat16_info_t fat16_info_array[MAX_FS];
static uint8_t current_fat16 = 0;

typedef struct
{
  fat16_info_t *info;
  uint16_t start_of_file;
} fat16_node_info_t;

static size_t fat16_file_read(vfs_file_t *file, void *buff, size_t len);
static size_t fat16_file_write(vfs_file_t *file, const void *buff, size_t len);
static int fat16_file_open(vfs_vnode_t *vnode);
static int fat16_file_close(vfs_file_t *file);

typedef void (* fat16_directory_foreach_t) (vfs_vnode_t *node,
                                            fat16_dirent_t *entry,
                                            void *data);

static int fat16_directory_readdir(vfs_vnode_t *node,
                                   size_t *files_in_dir,
                                   vfs_vnode_t ***files);


static vfs_fileops_t fat16_file_fileops =
{
  .read = fat16_file_read,
  .write = fat16_file_write,
  .open = fat16_file_open,
  .close = fat16_file_close
};

static vfs_fileops_t fat16_directory_fileops =
{
  .readdir = fat16_directory_readdir
};


static uint32_t
fat16_get_cluster_offset (fat16_info_t *fs,
                          int           cluster)
{
  return fs->data_start_offset + (cluster - 2) * fs->cluster_size;
}

static char *
fat16_filename_from_dirent (fat16_dirent_t *entry,
                            char            buffer[16])
{
  int len;

  strlcpy (buffer, entry->name, 9);
  buffer[8] = 0;

  for (int i = 0; i < 8; i++)
    {
      if (buffer[i] == ' ')
        break;

      buffer[i] = tolower(buffer[i]);
    }

  len = 8;
  while (len > 1 && buffer[len - 1] == ' ')
    len--;

  if (entry->extension[0] == ' ')
    buffer[len] = 0;
  else
    {
      buffer[len++] = '.';

      strlcpy (buffer + len, entry->extension, 3);
      buffer[len+3] = 0;

      for (int i = len; i < 3; i++)
        {
          if (buffer[i] == ' ')
            break;

          buffer[i] = tolower(buffer[i]);
        }
    }

  return buffer;
}

static void
fat16_directory_readdir_count_foreach(vfs_vnode_t *node,
                                      fat16_dirent_t *entry,
                                      void *data)
{
  node->special.dir.files_in_dir++;
}

static void
fat16_directory_readdir_create_foreach(vfs_vnode_t *node,
                                       fat16_dirent_t *entry,
                                       void *data)
{
  vfs_vnode_t *vnode;
  fat16_node_info_t *new_node_info;
  fat16_node_info_t *ninfo = (fat16_node_info_t *)&node->fs_data;
  fat16_info_t *info = ninfo->info;
  uint16_t *processed = (uint16_t *)data;

  vnode = node->special.dir.files[(*processed)++] = vfs_mknode();
  fat16_filename_from_dirent(entry, vnode->name);
  if (entry->attribute & FAT16_DIRECTORY)
    {
      vnode->type = VFS_DIR;
      vnode->special.dir.files_in_dir = 0;
      vnode->special.dir.files = NULL;
      vnode->fops = &fat16_directory_fileops;
    }
  else
    {
      vnode->type = VFS_DATA;
      vnode->fops = &fat16_file_fileops;
    }
  vnode->size = entry->file_size;
  vnode->parent = node;

  new_node_info = (fat16_node_info_t *)&vnode->fs_data;
  new_node_info->info = info;
  new_node_info->start_of_file = entry->start_of_file;
}

static void
fat16_directory_foreach(vfs_vnode_t *node,
                        fat16_directory_foreach_t foreach_func,
                        void *foreach_data)
{
  uint16_t i, e;
  uint32_t offset;
  uint32_t max_entries;
  uint16_t buffer_entries = 512 / 32; /* must be power of 2 */
  char buffer[512];
  fat16_dirent_t *entries;
  fat16_node_info_t *ninfo = (fat16_node_info_t *)&node->fs_data;
  fat16_info_t *info = ninfo->info;

  entries = (fat16_dirent_t *)&buffer[0];
  if (node->parent && node->parent->type == VFS_MOUNT)
    {
      offset = info->root_offset;
      max_entries = info->super.max_root_entries;
    }
  else
    {
      /* FIXME: Reading subdirectories is limited to a single cluster */
      offset = fat16_get_cluster_offset(info, ninfo->start_of_file);
      max_entries = info->cluster_size / 32;
    }

  for (i = 0, e = 0; i < max_entries; i++, e++)
    {
      fat16_dirent_t *entry;

      if (i == 0 || e == buffer_entries)
        {
          dev_seek(info->dev, info->minor, offset, SEEK_SET);
          dev_read(info->dev, info->minor, buffer, 512);

          offset += 512;
          e = 0;
        }

      entry = &entries[e];

      if (entry->name[0] == FAT16_ENTRY_AVAILABLE_LAST)
        break;
      else if ((uint8_t)entry->name[0] == FAT16_AVAILABLE_ERASED)
        continue;

      if (entry->attribute == FAT16_LFN)
        continue;

      (* foreach_func) (node, entry, foreach_data);
    }
}

static int
fat16_directory_readdir(vfs_vnode_t *node,
                        size_t *files_in_dir,
                        vfs_vnode_t ***files)
{
  uint16_t processed = 0;

  if (node->type != VFS_DIR)
    return -1;

  if (node->special.dir.files_in_dir != 0
      && node->special.dir.files != NULL)
    {
      *files_in_dir = node->special.dir.files_in_dir;
      *files = node->special.dir.files;

      return 0;
    }

  /* Count #files in directory */
  node->special.dir.files_in_dir = 0;

  fat16_directory_foreach(node, fat16_directory_readdir_count_foreach, NULL);

  /* Reset state and now traverse for creating vnotes for directory items */
  node->special.dir.files = vfs_getdirp(node->special.dir.files_in_dir);

  fat16_directory_foreach(node, fat16_directory_readdir_create_foreach,
                          &processed);

  /* Set out parameters */
  *files_in_dir = node->special.dir.files_in_dir;
  *files = node->special.dir.files;

  return 0;
}

static size_t
fat16_transfer(dev_t *dev, dev_minor_t minor, off_t offset,
               char *buffer, size_t nbytes)
{
  dev_seek(dev, minor, offset, SEEK_SET);
  return dev_read(dev, minor, buffer, nbytes);
}

static uint16_t
fat16_get_current_cluster(fat16_node_info_t *ninfo, off_t off,
                          uint16_t *cluster_position)
{
  off_t o = 0;
  uint16_t cluster = ninfo->start_of_file;

  while (off >= o + ninfo->info->cluster_size)
    {
      cluster = ninfo->info->fat_table[cluster];
      if (cluster >= FAT16_EOC_LO && cluster <= FAT16_EOC_HI)
        return cluster;

      o += ninfo->info->cluster_size;
    }

  if (cluster_position)
    *cluster_position = off - o;

  return cluster;
}

static uint16_t
fat16_get_next_cluster(fat16_info_t *info, uint16_t current_cluster)
{
  uint16_t cluster = 0;

  if (current_cluster >= FAT16_EOC_LO && current_cluster <= FAT16_EOC_HI)
    return current_cluster;

  cluster = info->fat_table[current_cluster];

  return cluster;
}

static size_t
fat16_file_read(vfs_file_t *file, void *buff, size_t len)
{
  uint16_t cluster_position = 0;
  uint16_t cluster;
  size_t read = 0;
  fat16_node_info_t *ninfo;

  if (file->vnode->type == VFS_DIR)
    return -1;

  ninfo = (fat16_node_info_t *)&file->vnode->fs_data;
  cluster = fat16_get_current_cluster(ninfo, file->off, &cluster_position);

  while (len > 0 && cluster >= FAT16_USED_LO && cluster <= FAT16_USED_HI)
    {
      off_t offset;
      uint16_t transfer;

      offset = fat16_get_cluster_offset(ninfo->info, cluster);

      if (cluster_position == 0)
        transfer = ninfo->info->cluster_size;
      else
        transfer = ninfo->info->cluster_size - cluster_position;

      if (transfer > len)
        transfer = len;

      transfer = fat16_transfer(ninfo->info->dev, ninfo->info->minor,
                                offset + cluster_position, buff + read,
                                transfer);

      len -= transfer;
      read += transfer;
      file->off += transfer;

      if (len > 0)
        {
          cluster = fat16_get_next_cluster(ninfo->info, cluster);
          if (cluster > FAT16_EOC_LO)
            {
              printf("FAT16: error: Corrupted FAT.\n");
              break;
            }
        }
    }

  return read;
}

static size_t
fat16_file_write(vfs_file_t *file, const void *buff, size_t len)
{
  /* FIXME: Write not implemented yet. */
  return -1;
}

static int
fat16_file_open(vfs_vnode_t *vnode)
{
  if (vnode->type == VFS_DIR)
    return -1;

  return 0;
}

static int
fat16_file_close(vfs_file_t *file)
{
  return 0;
}



static bool
fat16_has_mbr (void *data)
{
  mbr_t *mbr = data;

  if (mbr->exec_marker == 0xaa55)
    return true;
  return false;
}

static bool
fat16_has_mbr_active (dev_t *dev, dev_minor_t minor)
{
  int i;
  int active = 0;
  char mbr_data[512];
  mbr_t *mbr = (mbr_t *)mbr_data;

  dev_seek(dev, minor, 0, SEEK_SET);
  dev_read(dev, minor, mbr_data, 512);

  if (!fat16_has_mbr (mbr_data))
    return false;

  for (i = 0; i < 4; i++)
    if (mbr->partitions[i].active == 0x80)
      active++;

  return active > 0;
}

#if 0
static void
print_mbr (Dev *dev)
{
  int i;
  char mbr_data[512];
  mbr_t *mbr = (mbr_t *)mbr_data;

  dev_seek(dev, 0, SEEK_SET);
  dev_read(dev, mbr_data, 512);

  if (mbr->exec_marker != 0xaa55)
    {
      printf ("No MBR present.\n");
      return;
    }

  printf ("Partitions in MBR:\n");
  for (i = 0; i < 4; i++)
    {
      printf ("  %d: ACT=%d TYPE=%d size=%d\n",
              i,
              mbr->partitions[i].active,
              mbr->partitions[i].type,
              mbr->partitions[i].num_sectors);
    }
}
#endif

static int
fat16_read_super (fat16_info_t *info)
{
  uint32_t fat_size;

  dev_seek(info->dev, info->minor, 0, SEEK_SET);
  dev_read(info->dev, info->minor, &info->super, sizeof(fat16_super_t));

  if (info->super.ext_signature != 0x29
      || info->super.exec_marker != 0xaa55)
    {
      puts("FAT16: error: Not a FAT16 partition.");
      panic();
      return -1;
    }

  info->fat_table_offset =
      (info->super.reserved_sectors * info->super.bytes_per_sector);
  info->root_offset = info->fat_table_offset +
    (info->super.sectors_per_fat * 2) * info->super.bytes_per_sector;
  info->data_start_offset = info->root_offset +
      info->super.max_root_entries * 32;

  info->cluster_size = info->super.bytes_per_sector *
      info->super.sectors_per_cluster;

  fat_size = info->super.sectors_per_fat * info->super.bytes_per_sector;
  info->fat_table = vm_map(vm_get_kernel_map(VM_REG_KERNEL_HEAP), VM_SUPER_RW, 0,
                           (fat_size / 4096 + 1) * 4096);
  dev_seek(info->dev, info->minor, info->fat_table_offset, SEEK_SET);
  dev_read(info->dev, info->minor, info->fat_table, fat_size);

  return 0;
}

static int
fat16_mount(vfs_filesys_t *fs,
            vfs_vnode_t *vnode,
            dev_t *dev,
            dev_minor_t minor)
{
  fat16_info_t *info = &fat16_info_array[current_fat16++];

  vnode->special.mount.fs_data = info;
  info->dev = dev;
  info->minor = minor;

  if (dev_open(dev, minor) < 0)
    {
      puts("FAT16: error: Cannot open device.");
      return -1;
    }

  if (fat16_has_mbr_active(dev, minor))
    {
      puts("FAT16: FIXME: No support for parsing MBR.");
      return -1;
    }

  fat16_read_super(info);

  return 0;
}

static int
fat16_umount(vfs_vnode_t *vnode)
{
  if (vnode->type != VFS_MOUNT)
    return -1;

  dev_close(vnode->special.mount.dev, vnode->special.mount.minor);

  vnode->special.mount.fs = NULL;
  vnode->special.mount.root = NULL;

  return 0;
}

static vfs_vnode_t *
fat16_get_root(vfs_vnode_t *mountpoint)
{
  vfs_vnode_t *root;
  fat16_node_info_t *ninfo;

  root = vfs_mknode();
  root->type = VFS_DIR;
  root->parent = mountpoint;
  root->name[0] = 0;
  root->fs = mountpoint->special.mount.fs;
  root->fops = &fat16_directory_fileops;

  ninfo = (fat16_node_info_t *)&root->fs_data;
  ninfo->info = mountpoint->special.mount.fs_data;

  root->special.dir.files_in_dir = 0;
  root->special.dir.files = NULL;

  return root;
}

static vfs_filesys_t fat16_fs =
{
  "FAT16",
  fat16_mount,
  fat16_umount,
  fat16_get_root
};

void
fat16_init(void)
{
  vfs_register(&fat16_fs);
}
